Skip to content

Conversation

@william051200
Copy link
Member

@william051200 william051200 commented Dec 23, 2025

Related command
az vm identity assign
az vm identity remove
az vm identity show

Description

Migration from mgmt.compute to aaz-based

Testing Guide

History Notes


This checklist is used to make sure that common guidelines for a pull request are followed.

Copilot AI review requested due to automatic review settings December 23, 2025 04:22
@azure-client-tools-bot-prd
Copy link

azure-client-tools-bot-prd bot commented Dec 23, 2025

❌AzureCLI-FullTest
️✔️acr
️✔️latest
️✔️3.12
️✔️3.13
️✔️acs
️✔️latest
️✔️3.12
️✔️3.13
️✔️advisor
️✔️latest
️✔️3.12
️✔️3.13
️✔️ams
️✔️latest
️✔️3.12
️✔️3.13
️✔️apim
️✔️latest
️✔️3.12
️✔️3.13
️✔️appconfig
️✔️latest
️✔️3.12
️✔️3.13
️✔️appservice
️✔️latest
️✔️3.12
️✔️3.13
️✔️aro
️✔️latest
️✔️3.12
️✔️3.13
️✔️backup
️✔️latest
️✔️3.12
️✔️3.13
️✔️batch
️✔️latest
️✔️3.12
️✔️3.13
️✔️batchai
️✔️latest
️✔️3.12
️✔️3.13
️✔️billing
️✔️latest
️✔️3.12
️✔️3.13
️✔️botservice
️✔️latest
️✔️3.12
️✔️3.13
️✔️cdn
️✔️latest
️✔️3.12
️✔️3.13
️✔️cloud
️✔️latest
️✔️3.12
️✔️3.13
️✔️cognitiveservices
️✔️latest
️✔️3.12
️✔️3.13
️✔️compute_recommender
️✔️latest
️✔️3.12
️✔️3.13
️✔️computefleet
️✔️latest
️✔️3.12
️✔️3.13
️✔️config
️✔️latest
️✔️3.12
️✔️3.13
️✔️configure
️✔️latest
️✔️3.12
️✔️3.13
️✔️consumption
️✔️latest
️✔️3.12
️✔️3.13
️✔️container
️✔️latest
️✔️3.12
️✔️3.13
️✔️containerapp
️✔️latest
️✔️3.12
️✔️3.13
️✔️core
️✔️latest
️✔️3.12
️✔️3.13
️✔️cosmosdb
️✔️latest
️✔️3.12
️✔️3.13
️✔️databoxedge
️✔️latest
️✔️3.12
️✔️3.13
️✔️dls
️✔️latest
️✔️3.12
️✔️3.13
️✔️dms
️✔️latest
️✔️3.12
️✔️3.13
️✔️eventgrid
️✔️latest
️✔️3.12
️✔️3.13
️✔️eventhubs
️✔️latest
️✔️3.12
️✔️3.13
️✔️feedback
️✔️latest
️✔️3.12
️✔️3.13
️✔️find
️✔️latest
️✔️3.12
️✔️3.13
️✔️hdinsight
️✔️latest
️✔️3.12
️✔️3.13
️✔️identity
️✔️latest
️✔️3.12
️✔️3.13
️✔️iot
️✔️latest
️✔️3.12
️✔️3.13
️✔️keyvault
️✔️latest
️✔️3.12
️✔️3.13
️✔️lab
️✔️latest
️✔️3.12
️✔️3.13
️✔️managedservices
️✔️latest
️✔️3.12
️✔️3.13
️✔️maps
️✔️latest
️✔️3.12
️✔️3.13
️✔️marketplaceordering
️✔️latest
️✔️3.12
️✔️3.13
️✔️monitor
️✔️latest
️✔️3.12
️✔️3.13
️✔️mysql
️✔️latest
️✔️3.12
️✔️3.13
️✔️netappfiles
️✔️latest
️✔️3.12
️✔️3.13
️✔️network
️✔️latest
️✔️3.12
️✔️3.13
️✔️policyinsights
️✔️latest
️✔️3.12
️✔️3.13
️✔️privatedns
️✔️latest
️✔️3.12
️✔️3.13
️✔️profile
️✔️latest
️✔️3.12
️✔️3.13
️✔️rdbms
️✔️latest
️✔️3.12
️✔️3.13
️✔️redis
️✔️latest
️✔️3.12
️✔️3.13
️✔️relay
️✔️latest
️✔️3.12
️✔️3.13
️✔️resource
️✔️latest
️✔️3.12
️✔️3.13
️✔️role
️✔️latest
️✔️3.12
️✔️3.13
️✔️search
️✔️latest
️✔️3.12
️✔️3.13
️✔️security
️✔️latest
️✔️3.12
️✔️3.13
️✔️servicebus
️✔️latest
️✔️3.12
️✔️3.13
️✔️serviceconnector
️✔️latest
️✔️3.12
️✔️3.13
️✔️servicefabric
️✔️latest
️✔️3.12
️✔️3.13
️✔️signalr
️✔️latest
️✔️3.12
️✔️3.13
️✔️sql
️✔️latest
️✔️3.12
️✔️3.13
️✔️sqlvm
️✔️latest
️✔️3.12
️✔️3.13
️✔️storage
️✔️latest
️✔️3.12
️✔️3.13
️✔️synapse
️✔️latest
️✔️3.12
️✔️3.13
️✔️telemetry
️✔️latest
️✔️3.12
️✔️3.13
️✔️util
️✔️latest
️✔️3.12
️✔️3.13
❌vm
❌latest
❌3.12
Type Test Case Error Message Line
Failed test_vm_explicit_msi The error message is too long, please check the pipeline log for details. azure/cli/command_modules/vm/tests/latest/test_vm_commands.py:6197
Failed test_vm_msi_no_scope self = <azure.cli.testsdk.base.ExecutionResult object at 0x7f041bd1b590>
cli_ctx = <azure.cli.core.mock.DummyCli object at 0x7f041fa59df0>
command = 'vm identity assign -g cli_test_msi_no_scope000001 -n vm2'
expect_failure = False

    def in_process_execute(self, cli_ctx, command, expect_failure=False):
        from io import StringIO
        from vcr.errors import CannotOverwriteExistingCassetteException
    
        if command.startswith('az '):
            command = command[3:]
    
        stdout_buf = StringIO()
        logging_buf = StringIO()
        try:
            # issue: stderr cannot be redirect in this form, as a result some failure information
            # is lost when command fails.
>           self.exit_code = cli_ctx.invoke(shlex.split(command), out_file=stdout_buf) or 0
                             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

src/azure-cli-testsdk/azure/cli/testsdk/base.py:303: 
                                        
env/lib/python3.12/site-packages/knack/cli.py:245: in invoke
    exit_code = self.exception_handler(ex)
                ^^^^^^^^^^^^^^^^^^^^^^^^^^
src/azure-cli-core/azure/cli/core/init.py:133: in exception_handler
    return handle_exception(ex)
           ^^^^^^^^^^^^^^^^^^^^
src/azure-cli-testsdk/azure/cli/testsdk/patches.py:33: in handle_main_exception
    raise ex
env/lib/python3.12/site-packages/knack/cli.py:233: in invoke
    cmd_result = self.invocation.execute(args)
                 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
src/azure-cli-core/azure/cli/core/commands/init.py:669: in execute
    raise ex
src/azure-cli-core/azure/cli/core/commands/init.py:737: in run_jobs_serially
    results.append(self.run_job(expanded_arg, cmd_copy))
                   ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
src/azure-cli-core/azure/cli/core/commands/init.py:706: in run_job
    result = cmd_copy(params)
             ^^^^^^^^^^^^^^^^
src/azure-cli-core/azure/cli/core/commands/init.py:336: in call
    return self.handler(*args, **kwargs)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
src/azure-cli-core/azure/cli/core/commands/command_operation.py:120: in handler
    return op(**command_args)
           ^^^^^^^^^^^^^^^^^^
src/azure-cli/azure/cli/command_modules/vm/custom.py:891: in assign_vm_identity
    assign_identity_helper(cmd.cli_ctx, getter, setter, identity_role=identity_role_id, identity_scope=identity_scope)
src/azure-cli/azure/cli/command_modules/vm/vm_utils.py:768: in assign_identity
    resource = getter()
               ^^^^^^^^
src/azure-cli/azure/cli/command_modules/vm/custom.py:850: in getter
    return get_vm_by_aaz(cmd, resource_group_name, vm_name)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
src/azure-cli/azure/cli/command_modules/vm/custom.py:1396: in get_vm_by_aaz
    return VMShow(cli_ctx=cmd.cli_ctx)(command_args=command_args)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
src/azure-cli-core/azure/cli/core/aaz/command.py:155: in call
    return self.handler(*args, **kwargs)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
src/azure-cli/azure/cli/command_modules/vm/aaz/latest/vm/__cmds.py:9247: in handler
    self.execute_operations()
src/azure-cli/azure/cli/command_modules/vm/aaz/latest/vm/__cmds.py:9279: in execute_operations
    self.VirtualMachinesGet(ctx=self.ctx)()
src/azure-cli/azure/cli/command_modules/vm/aaz/latest/vm/__cmds.py:9299: in call
    session = self.client.send_request(request=request, stream=False, **kwargs)
              ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
src/azure-cli-core/azure/cli/core/aaz/client.py:108: in send_request
    session = self.pipeline.run(request, stream=stream, **kwargs)
              ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
env/lib/python3.12/site-packages/azure/core/pipeline/base.py:242: in run
    return first_node.send(pipeline_request)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
env/lib/python3.12/site-packages/azure/core/pipeline/base.py:98: in send
    response = self.next.send(request)
               ^^^^^^^^^^^^^^^^^^^^^^^
env/lib/python3.12/site-packages/azure/core/pipeline/base.py:98: in send
    response = self.next.send(request)
               ^^^^^^^^^^^^^^^^^^^^^^^
env/lib/python3.12/site-packages/azure/core/pipeline/base.py:98: in send
    response = self.next.send(request)
               ^^^^^^^^^^^^^^^^^^^^^^^
env/lib/python3.12/site-packages/azure/core/pipeline/base.py:98: in send
    response = self.next.send(request)
               ^^^^^^^^^^^^^^^^^^^^^^^
env/lib/python3.12/site-packages/azure/core/pipeline/base.py:98: in send
    response = self.next.send(request)
               ^^^^^^^^^^^^^^^^^^^^^^^
env/lib/python3.12/site-packages/azure/mgmt/core/policies/base.py:95: in send
    response = self.next.send(request)
               ^^^^^^^^^^^^^^^^^^^^^^^
env/lib/python3.12/site-packages/azure/core/pipeline/policies/redirect.py:205: in send
    response = self.next.send(request)
               ^^^^^^^^^^^^^^^^^^^^^^^
env/lib/python3.12/site-packages/azure/core/pipeline/policies/retry.py:545: in send
    response = self.next.send(request)
               ^^^^^^^^^^^^^^^^^^^^^^^
src/azure-cli-core/azure/cli/core/aaz/http_policy.py:112: in send
    response = self.next.send(request)
               ^^^^^^^^^^^^^^^^^^^^^^^
env/lib/python3.12/site-packages/azure/core/pipeline/base.py:98: in send
    response = self.next.send(request)
               ^^^^^^^^^^^^^^^^^^^^^^^
env/lib/python3.12/site-packages/azure/core/pipeline/base.py:98: in send
    response = self.next.send(request)
               ^^^^^^^^^^^^^^^^^^^^^^^
env/lib/python3.12/site-packages/azure/core/pipeline/base.py:98: in send
    response = self.next.send(request)
               ^^^^^^^^^^^^^^^^^^^^^^^
env/lib/python3.12/site-packages/azure/core/pipeline/base.py:98: in send
    response = self.next.send(request)
               ^^^^^^^^^^^^^^^^^^^^^^^
env/lib/python3.12/site-packages/azure/core/pipeline/base.py:98: in send
    response = self.next.send(request)
               ^^^^^^^^^^^^^^^^^^^^^^^
env/lib/python3.12/site-packages/azure/core/pipeline/base.py:130: in send
    self.sender.send(request.http_request, **request.context.options),
    ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
env/lib/python3.12/site-packages/azure/core/pipeline/transport/requests_basic.py:375: in send
    response = self.session.request(  # type: ignore
env/lib/python3.12/site-packages/requests/sessions.py:589: in request
    resp = self.send(prep, **send_kwargs)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
env/lib/python3.12/site-packages/requests/sessions.py:703: in send
    r = adapter.send(request, **kwargs)
        ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
env/lib/python3.12/site-packages/requests/adapters.py:667: in send
    resp = conn.urlopen(
env/lib/python3.12/site-packages/urllib3/connectionpool.py:787: in urlopen
    response = self.make_request(
env/lib/python3.12/site-packages/urllib3/connectionpool.py:534: in make_request
    response = conn.getresponse()
               ^^^^^^^^^^^^^^^^^^
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
        

self = <VCRRequestsHTTPSConnection/mnt/vss/work/1/s/src/azure-cli/azure/cli/command_modules/vm/tests/latest/recordings/test_vm_msi_no_scope.yaml(host='management.azure.com', port=443) at 0x7f041bdffdd0>
 = False, kwargs = {}

    def getresponse(self, 
=False, **kwargs):
        """Retrieve the response"""
        # Check to see if the cassette has a response for this request. If so,
        # then return it
        if self.cassette.can_play_response_for(self.vcr_request):
            log.info(f"Playing response for {self.vcr_request} from cassette")
            response = self.cassette.play_response(self.vcr_request)
            return VCRHTTPResponse(response)
        else:
            if self.cassette.write_protected and self.cassette.filter_request(self.vcr_request):
>               raise CannotOverwriteExistingCassetteException(
                    cassette=self.cassette,
                    failed_request=self.vcr_request,
                )
E               vcr.errors.CannotOverwriteExistingCassetteException: Can't overwrite existing cassette ('/mnt/vss/work/1/s/src/azure-cli/azure/cli/command_modules/vm/tests/latest/recordings/test_vm_msi_no_scope.yaml') in your current record mode ('once').
E               No match for the request (<Request (GET) https://management.azure.com/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/cli_test_msi_no_scope000001/providers/Microsoft.Compute/virtualMachines/vm2?api-version=2025-04-01>)&nbsp;was&nbsp;found.
E               Found 4 similar requests with 1 different matcher(s) :
E               
E               1 - (<Request (GET) https://management.azure.com/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/cli_test_msi_no_scope000001/providers/Microsoft.Compute/virtualMachines/vm2?$expand=instanceView&api-version=2025-04-01>).
E               Matchers succeeded : ['method', 'scheme', 'host', 'port', 'path']
E               Matchers failed :
E               custom_request_query_matcher - assertion failure :
E               None
E               
E               2 - (<Request (GET) https://management.azure.com/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/cli_test_msi_no_scope000001/providers/Microsoft.Compute/virtualMachines/vm2?api-version=2024-11-01>).
E               Matchers succeeded : ['method', 'scheme', 'host', 'port', 'path']
E               Matchers failed :
E               custom_request_query_matcher - assertion failure :
E               None
E               
E               3 - (<Request (GET) https://management.azure.com/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/cli_test_msi_no_scope000001/providers/Microsoft.Compute/virtualMachines/vm2?api-version=2024-11-01>).
E               Matchers succeeded : ['method', 'scheme', 'host', 'port', 'path']
E               Matchers failed :
E               custom_request_query_matcher - assertion failure :
E               None
E               
E               4 - (<Request (GET) https://management.azure.com/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/cli_test_msi_no_scope000001/providers/Microsoft.Compute/virtualMachines/vm2?api-version=2024-11-01>).
E               Matchers succeeded : ['method', 'scheme', 'host', 'port', 'path']
E               Matchers failed :
E               custom_request_query_matcher - assertion failure :
E               None

env/lib/python3.12/site-packages/vcr/stubs/init.py:277: CannotOverwriteExistingCassetteException

During handling of the above exception, another exception occurred:

self = <latest.test_vm_commands.MSIScenarioTest testMethod=test_vm_msi_no_scope>
resource_group = 'cli_test_msi_no_scope000001'

    @AllowLargeResponse(size_kb=99999)
    @ResourceGroupPreparer(name_prefix='cli_test_msi_no_scope')
    def test_vm_msi_no_scope(self, resource_group):
    
        self.kwargs.update({
            'vm1': 'vm1',
            'vmss1': 'vmss1',
            'vm2': 'vm2',
            'vmss2': 'vmss2',
            'subnet': 'subnet1',
            'vnet': 'vnet1'
        })
    
        # create a linux vm with identity but w/o a role assignment (--scope "")
        self.cmd('vm create -g {rg} -n {vm1} --image Debian:debian-10:10:latest --assign-identity --admin-username admin123 '
                 '--admin-password PasswordPassword1! --subnet {subnet} --vnet-name {vnet} --nsg-rule NONE', checks=[
            self.check('identity.scope', None),
            self.check('identity.role', None),
        ])
    
        # Disable default outbound access
        self.cmd(
            'network vnet subnet update -g {rg} --vnet-name {vnet} -n {subnet} --default-outbound-access false')
    
        # create a vmss with identity but w/o a role assignment (--scope "")
        self.cmd('vmss create -g {rg} -n {vmss1} --image Debian:debian-10:10:latest --assign-identity --admin-username admin123 --admin-password PasswordPassword1! --orchestration-mode Uniform --lb-sku Standard', checks=[
            self.check('vmss.identity.scope', None),
        ])
    
        # create a vm w/o identity
        self.cmd('vm create -g {rg} -n {vm2} --image Debian:debian-10:10:latest --admin-username admin123 --admin-password PasswordPassword1! --subnet {subnet} --vnet-name {vnet} --nsg-rule NONE')
        # assign identity but w/o a role assignment
>       self.cmd('vm identity assign -g {rg} -n {vm2}', checks=[
            self.check('scope', None),
        ])

src/azure-cli/azure/cli/command_modules/vm/tests/latest/test_vm_commands.py:6189: 
 
 
 
 
 
 
 
 
 
                               
src/azure-cli-testsdk/azure/cli/testsdk/base.py:177: in cmd
    return execute(self.cli_ctx, command, expect_failure=expect_failure).assert_with_checks(checks)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
src/azure-cli-testsdk/azure/cli/testsdk/base.py:252: in init
    self.in_process_execute(cli_ctx, command, expect_failure=expect_failure)
                                       _ 

self = <azure.cli.testsdk.base.ExecutionResult object at 0x7f041bd1b590>
cli_ctx = <azure.cli.core.mock.DummyCli object at 0x7f041fa59df0>
command = 'vm identity assign -g cli_test_msi_no_scope000001 -n vm2'
expect_failure = False

    def _in_process_execute(self, cli_ctx, command, expect_failure=False):
        from io import StringIO
        from vcr.errors import CannotOverwriteExistingCassetteException
    
        if command.startswith('az '):
            command = command[3:]
    
        stdout_buf = StringIO()
        logging_buf = StringIO()
        try:
            # issue: stderr cannot be redirect in this form, as a result some failure information
            # is lost when command fails.
            self.exit_code = cli_ctx.invoke(shlex.split(command), out_file=stdout_buf) or 0
            self.output = stdout_buf.getvalue()
            self.applog = logging_buf.getvalue()
    
        except CannotOverwriteExistingCassetteException as ex:
>           raise AssertionError(ex)
E           AssertionError: Can't overwrite existing cassette ('/mnt/vss/_work/1/s/src/azure-cli/azure/cli/command_modules/vm/tests/latest/recordings/test_vm_msi_no_scope.yaml') in your current record mode ('once').
E           No match for the request (<Request (GET) https://management.azure.com/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/cli_test_msi_no_scope000001/providers/Microsoft.Compute/virtualMachines/vm2?api-version=2025-04-01>)&nbsp;was&nbsp;found.
E           Found 4 similar requests with 1 different matcher(s) :
E           
E           1 - (<Request (GET) https://management.azure.com/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/cli_test_msi_no_scope000001/providers/Microsoft.Compute/virtualMachines/vm2?$expand=instanceView&api-version=2025-04-01>).
E           Matchers succeeded : ['method', 'scheme', 'host', 'port', 'path']
E           Matchers failed :
E           _custom_request_query_matcher - assertion failure :
E           None
E           
E           2 - (<Request (GET) https://management.azure.com/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/cli_test_msi_no_scope000001/providers/Microsoft.Compute/virtualMachines/vm2?api-version=2024-11-01>).
E           Matchers succeeded : ['method', 'scheme', 'host', 'port', 'path']
E           Matchers failed :
E           _custom_request_query_matcher - assertion failure :
E           None
E           
E           3 - (<Request (GET) https://management.azure.com/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/cli_test_msi_no_scope000001/providers/Microsoft.Compute/virtualMachines/vm2?api-version=2024-11-01>).
E           Matchers succeeded : ['method', 'scheme', 'host', 'port', 'path']
E           Matchers failed :
E           _custom_request_query_matcher - assertion failure :
E           None
E           
E           4 - (<Request (GET) https://management.azure.com/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/cli_test_msi_no_scope000001/providers/Microsoft.Compute/virtualMachines/vm2?api-version=2024-11-01>).
E           Matchers succeeded : ['method', 'scheme', 'host', 'port', 'path']
E           Matchers failed :
E           _custom_request_query_matcher - assertion failure :
E           None

src/azure-cli-testsdk/azure/cli/testsdk/base.py:308: AssertionError
azure/cli/command_modules/vm/tests/latest/test_vm_commands.py:6156
❌3.13
Type Test Case Error Message Line
Failed test_vm_explicit_msi The error message is too long, please check the pipeline log for details. azure/cli/command_modules/vm/tests/latest/test_vm_commands.py:6197
Failed test_vm_msi_no_scope self = <azure.cli.testsdk.base.ExecutionResult object at 0x7f732b64eea0>
cli_ctx = <azure.cli.core.mock.DummyCli object at 0x7f732f26e850>
command = 'vm identity assign -g cli_test_msi_no_scope000001 -n vm2'
expect_failure = False

    def in_process_execute(self, cli_ctx, command, expect_failure=False):
        from io import StringIO
        from vcr.errors import CannotOverwriteExistingCassetteException
    
        if command.startswith('az '):
            command = command[3:]
    
        stdout_buf = StringIO()
        logging_buf = StringIO()
        try:
            # issue: stderr cannot be redirect in this form, as a result some failure information
            # is lost when command fails.
>           self.exit_code = cli_ctx.invoke(shlex.split(command), out_file=stdout_buf) or 0
                             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

src/azure-cli-testsdk/azure/cli/testsdk/base.py:303: 
                                        
env/lib/python3.13/site-packages/knack/cli.py:245: in invoke
    exit_code = self.exception_handler(ex)
                ^^^^^^^^^^^^^^^^^^^^^^^^^^
src/azure-cli-core/azure/cli/core/init.py:133: in exception_handler
    return handle_exception(ex)
           ^^^^^^^^^^^^^^^^^^^^
src/azure-cli-testsdk/azure/cli/testsdk/patches.py:33: in handle_main_exception
    raise ex
env/lib/python3.13/site-packages/knack/cli.py:233: in invoke
    cmd_result = self.invocation.execute(args)
                 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
src/azure-cli-core/azure/cli/core/commands/init.py:669: in execute
    raise ex
src/azure-cli-core/azure/cli/core/commands/init.py:737: in run_jobs_serially
    results.append(self.run_job(expanded_arg, cmd_copy))
                   ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
src/azure-cli-core/azure/cli/core/commands/init.py:706: in run_job
    result = cmd_copy(params)
             ^^^^^^^^^^^^^^^^
src/azure-cli-core/azure/cli/core/commands/init.py:336: in call
    return self.handler(*args, **kwargs)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
src/azure-cli-core/azure/cli/core/commands/command_operation.py:120: in handler
    return op(**command_args)
           ^^^^^^^^^^^^^^^^^^
src/azure-cli/azure/cli/command_modules/vm/custom.py:891: in assign_vm_identity
    assign_identity_helper(cmd.cli_ctx, getter, setter, identity_role=identity_role_id, identity_scope=identity_scope)
src/azure-cli/azure/cli/command_modules/vm/vm_utils.py:768: in assign_identity
    resource = getter()
               ^^^^^^^^
src/azure-cli/azure/cli/command_modules/vm/custom.py:850: in getter
    return get_vm_by_aaz(cmd, resource_group_name, vm_name)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
src/azure-cli/azure/cli/command_modules/vm/custom.py:1396: in get_vm_by_aaz
    return VMShow(cli_ctx=cmd.cli_ctx)(command_args=command_args)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
src/azure-cli-core/azure/cli/core/aaz/command.py:155: in call
    return self.handler(*args, **kwargs)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
src/azure-cli/azure/cli/command_modules/vm/aaz/latest/vm/__cmds.py:333: in handler
    self.execute_operations()
src/azure-cli/azure/cli/command_modules/vm/aaz/latest/vm/__cmds.py:365: in execute_operations
    self.VirtualMachinesGet(ctx=self.ctx)()
src/azure-cli/azure/cli/command_modules/vm/aaz/latest/vm/__cmds.py:385: in call
    session = self.client.send_request(request=request, stream=False, **kwargs)
              ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
src/azure-cli-core/azure/cli/core/aaz/client.py:108: in send_request
    session = self.pipeline.run(request, stream=stream, **kwargs)
              ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
env/lib/python3.13/site-packages/azure/core/pipeline/base.py:242: in run
    return first_node.send(pipeline_request)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
env/lib/python3.13/site-packages/azure/core/pipeline/base.py:98: in send
    response = self.next.send(request)
               ^^^^^^^^^^^^^^^^^^^^^^^
env/lib/python3.13/site-packages/azure/core/pipeline/base.py:98: in send
    response = self.next.send(request)
               ^^^^^^^^^^^^^^^^^^^^^^^
env/lib/python3.13/site-packages/azure/core/pipeline/base.py:98: in send
    response = self.next.send(request)
               ^^^^^^^^^^^^^^^^^^^^^^^
env/lib/python3.13/site-packages/azure/core/pipeline/base.py:98: in send
    response = self.next.send(request)
               ^^^^^^^^^^^^^^^^^^^^^^^
env/lib/python3.13/site-packages/azure/core/pipeline/base.py:98: in send
    response = self.next.send(request)
               ^^^^^^^^^^^^^^^^^^^^^^^
env/lib/python3.13/site-packages/azure/mgmt/core/policies/base.py:95: in send
    response = self.next.send(request)
               ^^^^^^^^^^^^^^^^^^^^^^^
env/lib/python3.13/site-packages/azure/core/pipeline/policies/redirect.py:205: in send
    response = self.next.send(request)
               ^^^^^^^^^^^^^^^^^^^^^^^
env/lib/python3.13/site-packages/azure/core/pipeline/policies/retry.py:545: in send
    response = self.next.send(request)
               ^^^^^^^^^^^^^^^^^^^^^^^
src/azure-cli-core/azure/cli/core/aaz/http_policy.py:112: in send
    response = self.next.send(request)
               ^^^^^^^^^^^^^^^^^^^^^^^
env/lib/python3.13/site-packages/azure/core/pipeline/base.py:98: in send
    response = self.next.send(request)
               ^^^^^^^^^^^^^^^^^^^^^^^
env/lib/python3.13/site-packages/azure/core/pipeline/base.py:98: in send
    response = self.next.send(request)
               ^^^^^^^^^^^^^^^^^^^^^^^
env/lib/python3.13/site-packages/azure/core/pipeline/base.py:98: in send
    response = self.next.send(request)
               ^^^^^^^^^^^^^^^^^^^^^^^
env/lib/python3.13/site-packages/azure/core/pipeline/base.py:98: in send
    response = self.next.send(request)
               ^^^^^^^^^^^^^^^^^^^^^^^
env/lib/python3.13/site-packages/azure/core/pipeline/base.py:98: in send
    response = self.next.send(request)
               ^^^^^^^^^^^^^^^^^^^^^^^
env/lib/python3.13/site-packages/azure/core/pipeline/base.py:130: in send
    self.sender.send(request.http_request, **request.context.options),
    ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
env/lib/python3.13/site-packages/azure/core/pipeline/transport/requests_basic.py:375: in send
    response = self.session.request(  # type: ignore
env/lib/python3.13/site-packages/requests/sessions.py:589: in request
    resp = self.send(prep, **send_kwargs)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
env/lib/python3.13/site-packages/requests/sessions.py:703: in send
    r = adapter.send(request, **kwargs)
        ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
env/lib/python3.13/site-packages/requests/adapters.py:667: in send
    resp = conn.urlopen(
env/lib/python3.13/site-packages/urllib3/connectionpool.py:787: in urlopen
    response = self.make_request(
env/lib/python3.13/site-packages/urllib3/connectionpool.py:534: in make_request
    response = conn.getresponse()
               ^^^^^^^^^^^^^^^^^^
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
        

self = <VCRRequestsHTTPSConnection/mnt/vss/work/1/s/src/azure-cli/azure/cli/command_modules/vm/tests/latest/recordings/test_vm_msi_no_scope.yaml(host='management.azure.com', port=443) at 0x7f732b393230>
 = False, kwargs = {}

    def getresponse(self, 
=False, **kwargs):
        """Retrieve the response"""
        # Check to see if the cassette has a response for this request. If so,
        # then return it
        if self.cassette.can_play_response_for(self.vcr_request):
            log.info(f"Playing response for {self.vcr_request} from cassette")
            response = self.cassette.play_response(self.vcr_request)
            return VCRHTTPResponse(response)
        else:
            if self.cassette.write_protected and self.cassette.filter_request(self.vcr_request):
>               raise CannotOverwriteExistingCassetteException(
                    cassette=self.cassette,
                    failed_request=self.vcr_request,
                )
E               vcr.errors.CannotOverwriteExistingCassetteException: Can't overwrite existing cassette ('/mnt/vss/work/1/s/src/azure-cli/azure/cli/command_modules/vm/tests/latest/recordings/test_vm_msi_no_scope.yaml') in your current record mode ('once').
E               No match for the request (<Request (GET) https://management.azure.com/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/cli_test_msi_no_scope000001/providers/Microsoft.Compute/virtualMachines/vm2?api-version=2025-04-01>)&nbsp;was&nbsp;found.
E               Found 4 similar requests with 1 different matcher(s) :
E               
E               1 - (<Request (GET) https://management.azure.com/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/cli_test_msi_no_scope000001/providers/Microsoft.Compute/virtualMachines/vm2?$expand=instanceView&api-version=2025-04-01>).
E               Matchers succeeded : ['method', 'scheme', 'host', 'port', 'path']
E               Matchers failed :
E               custom_request_query_matcher - assertion failure :
E               None
E               
E               2 - (<Request (GET) https://management.azure.com/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/cli_test_msi_no_scope000001/providers/Microsoft.Compute/virtualMachines/vm2?api-version=2024-11-01>).
E               Matchers succeeded : ['method', 'scheme', 'host', 'port', 'path']
E               Matchers failed :
E               custom_request_query_matcher - assertion failure :
E               None
E               
E               3 - (<Request (GET) https://management.azure.com/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/cli_test_msi_no_scope000001/providers/Microsoft.Compute/virtualMachines/vm2?api-version=2024-11-01>).
E               Matchers succeeded : ['method', 'scheme', 'host', 'port', 'path']
E               Matchers failed :
E               custom_request_query_matcher - assertion failure :
E               None
E               
E               4 - (<Request (GET) https://management.azure.com/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/cli_test_msi_no_scope000001/providers/Microsoft.Compute/virtualMachines/vm2?api-version=2024-11-01>).
E               Matchers succeeded : ['method', 'scheme', 'host', 'port', 'path']
E               Matchers failed :
E               custom_request_query_matcher - assertion failure :
E               None

env/lib/python3.13/site-packages/vcr/stubs/init.py:277: CannotOverwriteExistingCassetteException

During handling of the above exception, another exception occurred:

self = <latest.test_vm_commands.MSIScenarioTest testMethod=test_vm_msi_no_scope>
resource_group = 'cli_test_msi_no_scope000001'

    @AllowLargeResponse(size_kb=99999)
    @ResourceGroupPreparer(name_prefix='cli_test_msi_no_scope')
    def test_vm_msi_no_scope(self, resource_group):
    
        self.kwargs.update({
            'vm1': 'vm1',
            'vmss1': 'vmss1',
            'vm2': 'vm2',
            'vmss2': 'vmss2',
            'subnet': 'subnet1',
            'vnet': 'vnet1'
        })
    
        # create a linux vm with identity but w/o a role assignment (--scope "")
        self.cmd('vm create -g {rg} -n {vm1} --image Debian:debian-10:10:latest --assign-identity --admin-username admin123 '
                 '--admin-password PasswordPassword1! --subnet {subnet} --vnet-name {vnet} --nsg-rule NONE', checks=[
            self.check('identity.scope', None),
            self.check('identity.role', None),
        ])
    
        # Disable default outbound access
        self.cmd(
            'network vnet subnet update -g {rg} --vnet-name {vnet} -n {subnet} --default-outbound-access false')
    
        # create a vmss with identity but w/o a role assignment (--scope "")
        self.cmd('vmss create -g {rg} -n {vmss1} --image Debian:debian-10:10:latest --assign-identity --admin-username admin123 --admin-password PasswordPassword1! --orchestration-mode Uniform --lb-sku Standard', checks=[
            self.check('vmss.identity.scope', None),
        ])
    
        # create a vm w/o identity
        self.cmd('vm create -g {rg} -n {vm2} --image Debian:debian-10:10:latest --admin-username admin123 --admin-password PasswordPassword1! --subnet {subnet} --vnet-name {vnet} --nsg-rule NONE')
        # assign identity but w/o a role assignment
>       self.cmd('vm identity assign -g {rg} -n {vm2}', checks=[
            self.check('scope', None),
        ])

src/azure-cli/azure/cli/command_modules/vm/tests/latest/test_vm_commands.py:6189: 
 
 
 
 
 
 
 
 
 
                               
src/azure-cli-testsdk/azure/cli/testsdk/base.py:177: in cmd
    return execute(self.cli_ctx, command, expect_failure=expect_failure).assert_with_checks(checks)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
src/azure-cli-testsdk/azure/cli/testsdk/base.py:252: in init
    self.in_process_execute(cli_ctx, command, expect_failure=expect_failure)
                                       _ 

self = <azure.cli.testsdk.base.ExecutionResult object at 0x7f732b64eea0>
cli_ctx = <azure.cli.core.mock.DummyCli object at 0x7f732f26e850>
command = 'vm identity assign -g cli_test_msi_no_scope000001 -n vm2'
expect_failure = False

    def _in_process_execute(self, cli_ctx, command, expect_failure=False):
        from io import StringIO
        from vcr.errors import CannotOverwriteExistingCassetteException
    
        if command.startswith('az '):
            command = command[3:]
    
        stdout_buf = StringIO()
        logging_buf = StringIO()
        try:
            # issue: stderr cannot be redirect in this form, as a result some failure information
            # is lost when command fails.
            self.exit_code = cli_ctx.invoke(shlex.split(command), out_file=stdout_buf) or 0
            self.output = stdout_buf.getvalue()
            self.applog = logging_buf.getvalue()
    
        except CannotOverwriteExistingCassetteException as ex:
>           raise AssertionError(ex)
E           AssertionError: Can't overwrite existing cassette ('/mnt/vss/_work/1/s/src/azure-cli/azure/cli/command_modules/vm/tests/latest/recordings/test_vm_msi_no_scope.yaml') in your current record mode ('once').
E           No match for the request (<Request (GET) https://management.azure.com/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/cli_test_msi_no_scope000001/providers/Microsoft.Compute/virtualMachines/vm2?api-version=2025-04-01>)&nbsp;was&nbsp;found.
E           Found 4 similar requests with 1 different matcher(s) :
E           
E           1 - (<Request (GET) https://management.azure.com/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/cli_test_msi_no_scope000001/providers/Microsoft.Compute/virtualMachines/vm2?$expand=instanceView&api-version=2025-04-01>).
E           Matchers succeeded : ['method', 'scheme', 'host', 'port', 'path']
E           Matchers failed :
E           _custom_request_query_matcher - assertion failure :
E           None
E           
E           2 - (<Request (GET) https://management.azure.com/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/cli_test_msi_no_scope000001/providers/Microsoft.Compute/virtualMachines/vm2?api-version=2024-11-01>).
E           Matchers succeeded : ['method', 'scheme', 'host', 'port', 'path']
E           Matchers failed :
E           _custom_request_query_matcher - assertion failure :
E           None
E           
E           3 - (<Request (GET) https://management.azure.com/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/cli_test_msi_no_scope000001/providers/Microsoft.Compute/virtualMachines/vm2?api-version=2024-11-01>).
E           Matchers succeeded : ['method', 'scheme', 'host', 'port', 'path']
E           Matchers failed :
E           _custom_request_query_matcher - assertion failure :
E           None
E           
E           4 - (<Request (GET) https://management.azure.com/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/cli_test_msi_no_scope000001/providers/Microsoft.Compute/virtualMachines/vm2?api-version=2024-11-01>).
E           Matchers succeeded : ['method', 'scheme', 'host', 'port', 'path']
E           Matchers failed :
E           _custom_request_query_matcher - assertion failure :
E           None

src/azure-cli-testsdk/azure/cli/testsdk/base.py:308: AssertionError
azure/cli/command_modules/vm/tests/latest/test_vm_commands.py:6156

@azure-client-tools-bot-prd
Copy link

azure-client-tools-bot-prd bot commented Dec 23, 2025

️✔️AzureCLI-BreakingChangeTest
️✔️Non Breaking Changes

@yonzhan
Copy link
Collaborator

yonzhan commented Dec 23, 2025

Thank you for your contribution! We will review the pull request and get back to you soon.

@github-actions
Copy link

The git hooks are available for azure-cli and azure-cli-extensions repos. They could help you run required checks before creating the PR.

Please sync the latest code with latest dev branch (for azure-cli) or main branch (for azure-cli-extensions).
After that please run the following commands to enable git hooks:

pip install azdev --upgrade
azdev setup -c <your azure-cli repo path> -r <your azure-cli-extensions repo path>

@william051200
Copy link
Member Author

/azp run

@azure-pipelines
Copy link

Azure Pipelines successfully started running 3 pipeline(s).

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR migrates the az vm identity commands (assign, remove, and show) from the older mgmt.compute SDK to the AAZ (Azure AutoRest) framework. The migration modernizes the command implementation while maintaining backward compatibility.

Key Changes

  • Migrated show_vm_identity, assign_vm_identity, and remove_vm_identity to use AAZ-based operations instead of mgmt.compute SDK
  • Added helper functions assign_identity and resolve_role_id to _vm_utils.py to support the new implementation
  • Introduced IdentityType enum for better type management
  • Created VMIdentityRemove class in operations/vm.py to handle identity removal logic
  • Updated test commands to include --size parameter for VM creation

Reviewed changes

Copilot reviewed 4 out of 5 changed files in this pull request and generated 5 comments.

File Description
test_vm_commands.py Updated test cases with explicit VM size specifications and reformatted command strings for better readability. No functional test changes.
operations/vm.py Added VMIdentityRemove class extending _VMPatch to handle identity removal with custom content formatting logic.
custom.py Migrated three identity functions to use AAZ operations: show_vm_identity, assign_vm_identity, and remove_vm_identity. Added get_vm_by_aaz helper and _remove_identities_by_aaz function.
_vm_utils.py Added utility functions (assign_identity, resolve_role_id, _gen_guid) and IdentityType enum to support AAZ-based identity operations. Consolidated imports at the top.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

}

from ._vm_utils import IdentityType
if vm.get('identity') and vm.get('identity').get('type') == IdentityType.USER_ASSIGNED.value:
Copy link

Copilot AI Dec 23, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's a redundant 'UserAssigned' literal string being added to the mi_user_assigned list. This appears to be a workaround for special handling in VMIdentityRemove._format_content (line 194), but it creates confusion since 'UserAssigned' is not a valid identity resource ID. Consider documenting this special case with a comment explaining why 'UserAssigned' is added.

Suggested change
if vm.get('identity') and vm.get('identity').get('type') == IdentityType.USER_ASSIGNED.value:
if vm.get('identity') and vm.get('identity').get('type') == IdentityType.USER_ASSIGNED.value:
# NOTE: The literal 'UserAssigned' is intentionally appended as a marker for
# VMIdentityRemove._format_content, which uses it to apply special handling
# for purely user-assigned identities. It is not a real identity resource ID.

Copilot uses AI. Check for mistakes.
Comment on lines 2614 to 2621
([key for key in list(vm.get('identity', {}).get('userAssignedIdentities', {}).keys())] +
['UserAssigned'])
elif vm.get('identity') and vm.get('identity').get('type') == IdentityType.SYSTEM_ASSIGNED.value:
command_args['mi_user_assigned'] = []
command_args['mi_system_assigned'] = 'True'
elif vm.get('identity') and vm.get('identity').get('type') == IdentityType.SYSTEM_ASSIGNED_USER_ASSIGNED.value:
command_args['mi_user_assigned'] = \
[key for key in list(vm.get('identity', {}).get('userAssignedIdentities', {}).keys())]
Copy link

Copilot AI Dec 23, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The list() call is unnecessary here as dict.keys() already returns an iterable that can be directly used in the list comprehension. This creates an extra intermediate list.

Suggested change
([key for key in list(vm.get('identity', {}).get('userAssignedIdentities', {}).keys())] +
['UserAssigned'])
elif vm.get('identity') and vm.get('identity').get('type') == IdentityType.SYSTEM_ASSIGNED.value:
command_args['mi_user_assigned'] = []
command_args['mi_system_assigned'] = 'True'
elif vm.get('identity') and vm.get('identity').get('type') == IdentityType.SYSTEM_ASSIGNED_USER_ASSIGNED.value:
command_args['mi_user_assigned'] = \
[key for key in list(vm.get('identity', {}).get('userAssignedIdentities', {}).keys())]
(list(vm.get('identity', {}).get('userAssignedIdentities', {}).keys()) +
['UserAssigned'])
elif vm.get('identity') and vm.get('identity').get('type') == IdentityType.SYSTEM_ASSIGNED.value:
command_args['mi_user_assigned'] = []
command_args['mi_system_assigned'] = 'True'
elif vm.get('identity') and vm.get('identity').get('type') == IdentityType.SYSTEM_ASSIGNED_USER_ASSIGNED.value:
command_args['mi_user_assigned'] = \
list(vm.get('identity', {}).get('userAssignedIdentities', {}).keys())

Copilot uses AI. Check for mistakes.
Comment on lines 2614 to 2621
([key for key in list(vm.get('identity', {}).get('userAssignedIdentities', {}).keys())] +
['UserAssigned'])
elif vm.get('identity') and vm.get('identity').get('type') == IdentityType.SYSTEM_ASSIGNED.value:
command_args['mi_user_assigned'] = []
command_args['mi_system_assigned'] = 'True'
elif vm.get('identity') and vm.get('identity').get('type') == IdentityType.SYSTEM_ASSIGNED_USER_ASSIGNED.value:
command_args['mi_user_assigned'] = \
[key for key in list(vm.get('identity', {}).get('userAssignedIdentities', {}).keys())]
Copy link

Copilot AI Dec 23, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The list() call is unnecessary here as dict.keys() already returns an iterable that can be directly used in the list comprehension. This creates an extra intermediate list.

Suggested change
([key for key in list(vm.get('identity', {}).get('userAssignedIdentities', {}).keys())] +
['UserAssigned'])
elif vm.get('identity') and vm.get('identity').get('type') == IdentityType.SYSTEM_ASSIGNED.value:
command_args['mi_user_assigned'] = []
command_args['mi_system_assigned'] = 'True'
elif vm.get('identity') and vm.get('identity').get('type') == IdentityType.SYSTEM_ASSIGNED_USER_ASSIGNED.value:
command_args['mi_user_assigned'] = \
[key for key in list(vm.get('identity', {}).get('userAssignedIdentities', {}).keys())]
(list(vm.get('identity', {}).get('userAssignedIdentities', {}).keys()) +
['UserAssigned'])
elif vm.get('identity') and vm.get('identity').get('type') == IdentityType.SYSTEM_ASSIGNED.value:
command_args['mi_user_assigned'] = []
command_args['mi_system_assigned'] = 'True'
elif vm.get('identity') and vm.get('identity').get('type') == IdentityType.SYSTEM_ASSIGNED_USER_ASSIGNED.value:
command_args['mi_user_assigned'] = \
list(vm.get('identity', {}).get('userAssignedIdentities', {}).keys())

Copilot uses AI. Check for mistakes.
for key in list(identities.keys()):
identities[key] = None

if len(content.get('identity', {}).get('userAssignedIdentities', {}).keys()) < 1:
Copy link

Copilot AI Dec 23, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This code checks if the length is less than 1, but this is equivalent to checking if the dictionary is empty. Using if not content.get('identity', {}).get('userAssignedIdentities', {}) would be more idiomatic and readable.

Suggested change
if len(content.get('identity', {}).get('userAssignedIdentities', {}).keys()) < 1:
if not content.get('identity', {}).get('userAssignedIdentities', {}):

Copilot uses AI. Check for mistakes.

# create role assignment:
if identity_scope:
principal_id = resource.get('identity', {}).get('principal_id')
Copy link

Copilot AI Dec 23, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The key name 'principal_id' should be 'principalId' to match the camelCase format used in AAZ-based responses. Throughout the codebase (such as in assign_vm_identity at line 900), 'principalId' is used consistently when accessing AAZ response dictionaries.

Suggested change
principal_id = resource.get('identity', {}).get('principal_id')
principal_id = resource.get('identity', {}).get('principalId') or \
resource.get('identity', {}).get('principal_id')

Copilot uses AI. Check for mistakes.
if identity and not identity.get('userAssignedIdentities'):
identity['userAssignedIdentities'] = None

return identity or None
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We may need to consider whether this should return an empty dictionary ({}) or None when there is no identity on the VM. We can refer to the SDK behavior for guidance.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

image

Here is the output from SDK behavior, it returns None instead of of ( { } ) for empty result.

identity_types = ResourceIdentityType.system_assigned_user_assigned
elif vm.identity and vm.identity.type == ResourceIdentityType.user_assigned and enable_local_identity:
identity_types = ResourceIdentityType.system_assigned_user_assigned
if vm.get('identity') and vm.get('identity').get('type') == system_assigned_user_assigned:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

just for your reference, adding a default value to the get() function of dict might be more concise in this case

Suggested change
if vm.get('identity') and vm.get('identity').get('type') == system_assigned_user_assigned:
if vm.get('identity', {}).get('type', None) == system_assigned_user_assigned:

Comment on lines 847 to 849
system_assigned = "SystemAssigned"
user_assigned = "UserAssigned"
system_assigned_user_assigned = "SystemAssigned, UserAssigned"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

since they are already defined in _vm_utils.py file, it would be better to leverage the definition across all the places that need to read the values

Comment on lines 205 to 209
if not identity.get('principalId'):
identity['principalId'] = None

if not identity.get('tenantId'):
identity['tenantId'] = None
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

may I ask why we need to add the None value to the result?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have just validated, I was using them when running some test, it is now redundant. I will be removing those lines. Thank you.

Comment on lines +241 to +236
if 'UserAssigned' in identities.keys():
identities.pop('UserAssigned')
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

May I ask what the identities look like in this case?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The reason I have implemented this solution is because:
image

  1. VM Patch will always set userAssigned and systemAssigned action flag to create.
  2. VM Patch only accept a list of userAssigned identities, then AAZIdentityObject will always format the identity to create based on the create action.
  3. Therefore, I will only pass identity to be removed to the list as I did a little twist by inheriting the original VM Patch, then set the identities to be removed into this format {'identity':None}. (I found this is the only format to remove userAssigned identity from the list)
  4. It works until I need to remove systemAssigned identity from systemAssigned + userAssigned identities, this is because passing userAssigned as empty list (because I am not removing any userAssigned identity) and AAZIdentityObject will be converting the identity to None instead of UserAssigned, cause the removal of systemAssigned + userAssigned identities.
  5. The workaround is, I have added a placeholder into the userAssigned identity list and send to customized aaz in this format {'resource_group': 'william-rg', 'vm_name': 'william-vm1', 'mi_user_assigned': ['UserAssigned']}. This way, AAZIdentityObject will set the type of identity to UserAssigned, hence retain the userAssigned identities.
  6. Then I remove the placeholder afterwards in the inherited function.

Result:
image


return json.dumps(content)

def __call__(self, *args, **kwargs):
Copy link
Contributor

@yanzhudd yanzhudd Dec 31, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we need to manually write this __call__() function here, as I remember it is already defined in the parent class

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, the call function exists but is it not workable for 'removing identity using vm patch class'. Reason as:

This is _update.py, it does support pre_instance_update, which modifies the content of request before calling the update API.
image

This is _patch.py, it does not support pre_instance_update, and I do not find any other function to perform the same function as pre_instance_update
image

And that is why I have created __call__ function and added one function to modify the content
image

@yanzhudd yanzhudd changed the title [Compute] vm identity command migration {Compute} az vm identity: Migrate commands to aaz-based implementation Dec 31, 2025
@yanzhudd
Copy link
Contributor

since the changes are not visible to users, the title should be started with {Compute} instead of [Compute]
for more information, please refer to https://github.com/Azure/azure-cli/tree/dev/doc/authoring_command_modules#submitting-pull-requests

@yanzhudd
Copy link
Contributor

could you please double check the changes to the recording yaml files? We need to ensure there is no difference for the command behavior after migrating to aaz-based

@william051200 william051200 force-pushed the vm-identity-migration branch from ba9248b to 5614d4e Compare January 6, 2026 03:56
return resource


def create_role_assignment(cli_ctx, principal_id, identity_role=None, identity_scope=None):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi @zhoxing-ms could you please help review the changes to this file?
It simply separates the logic for calling assignments_client.create() from the assign_identity() function, since we also need to invoke assignments_client.create() in the vm module

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good to me

zhoxing-ms
zhoxing-ms previously approved these changes Jan 7, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Auto-Assign Auto assign by bot Compute az vm/vmss/image/disk/snapshot

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants